feat: add stream subcommand for live terminal session broadcasting#278
Conversation
Co-authored-by: ibigbug <543405+ibigbug@users.noreply.github.com>
stream subcommand for live terminal session broadcasting
There was a problem hiding this comment.
Pull request overview
Adds a new PowerSession stream subcommand to broadcast a live ConPTY-backed terminal session to an asciinema-compatible server over WebSocket, including API support for creating/reconnecting to streams.
Changes:
- Introduces a
streamCLI subcommand with--command/-cand--idto create or reconnect to live streams. - Extends the
ApiService/AsciinemaAPI client with live-stream endpoints and helper methods (create stream, build WS URL, auth header). - Adds synchronous WebSocket support via
tungstenite(rustls) and implements the streaming loop (reset + stdout events).
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| Cargo.toml | Adds tungstenite dependency for WSS streaming support. |
| Cargo.lock | Locks new transitive deps for tungstenite/rustls stack. |
| src/main.rs | Wires up the stream subcommand and create/reconnect flow. |
| src/commands/stream.rs | Implements live streaming (PTY IO threads + WS text frames). |
| src/commands/mod.rs | Registers and re-exports the new Stream command and ApiService. |
| src/commands/api/mod.rs | Extends ApiService with stream methods and defines StreamInfo. |
| src/commands/api/asciinema.rs | Implements stream creation, WS URL derivation, and WS auth header. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| #[cfg(windows)] | ||
| let (cols, rows) = { | ||
| let t = crate::terminal::WindowsTerminal::new(None); | ||
| (t.width as u16, t.height as u16) | ||
| }; |
There was a problem hiding this comment.
Creating a WindowsTerminal::new(None) here just to read width/height has significant side effects (it creates a ConPTY and switches the console into raw mode). This introduces an extra PTY allocation and can alter the user’s console state before streaming even starts. Prefer a lightweight terminal-size query (e.g., expose a dedicated get_console_size helper from the terminal module) or pass a default size to create_stream and rely on the subsequent reset event to communicate the actual size.
| println!("Streaming. Watch at: {}", self.stream_url); | ||
| println!("Press Ctrl+D to stop."); | ||
| self.stream(); |
There was a problem hiding this comment.
The instruction Press Ctrl+D to stop. is not accurate on Windows consoles (EOF is typically Ctrl+Z then Enter), and the streaming session actually ends when the spawned shell/process exits. Update the prompt to reflect the correct key sequence / behavior, or document that the user should exit the shell/command to end streaming.
| let (mut ws, _) = | ||
| tungstenite::connect(request).expect("failed to connect to stream server"); | ||
|
|
There was a problem hiding this comment.
The WebSocket is only ever written to; no read loop is running. If the server sends control frames (e.g., Ping/Close), the client may not respond, which can cause the connection to be dropped or to hang during shutdown. Consider running a small read loop (in this or a separate thread) to handle Ping/Pong and Close frames and to drive the connection state machine while streaming.
| fn create_stream(&self, cols: u16, rows: u16) -> Option<StreamInfo> { | ||
| #[derive(Deserialize)] | ||
| struct CreateStreamResponse { | ||
| id: String, | ||
| url: String, | ||
| ws_producer_url: String, | ||
| } | ||
|
|
||
| let stream_url = format!("{}/api/streams", &self.config.api_server); | ||
| let body = serde_json::json!({ "cols": cols, "rows": rows }); | ||
| let res = match self | ||
| .http_client | ||
| .post(stream_url) | ||
| .json(&body) | ||
| .send() | ||
| { | ||
| Ok(r) => r, | ||
| Err(e) => { | ||
| println!("Failed to reach stream server: {}", e); | ||
| return None; | ||
| } | ||
| }; | ||
|
|
||
| if res.status().is_success() { | ||
| match res.json::<CreateStreamResponse>() { | ||
| Ok(resp) => Some(StreamInfo { | ||
| id: resp.id, | ||
| url: resp.url, | ||
| ws_producer_url: resp.ws_producer_url, | ||
| }), | ||
| Err(e) => { | ||
| println!("Failed to parse stream response: {}", e); | ||
| None | ||
| } | ||
| } | ||
| } else { | ||
| println!("Failed to create stream:"); | ||
| println!("{}", res.text().unwrap_or_default()); | ||
| None | ||
| } | ||
| } | ||
|
|
||
| fn get_stream_ws_url(&self, stream_id: &str) -> String { | ||
| // Derive the producer WebSocket URL from the server base URL and stream ID. | ||
| // The path follows the asciinema server convention: /ws/S/<stream_id> | ||
| let base = self | ||
| .config | ||
| .api_server | ||
| .trim_end_matches('/') | ||
| .replace("https://", "wss://") | ||
| .replace("http://", "ws://"); | ||
| format!("{}/ws/S/{}", base, stream_id) | ||
| } | ||
|
|
||
| fn get_auth_header(&self) -> String { | ||
| let cred = format!("user:{}", self.config.install_id); | ||
| format!("Basic {}", BASE64_STANDARD.encode(&cred)) | ||
| } |
There was a problem hiding this comment.
New API helpers create_stream, get_stream_ws_url, and get_auth_header were added, but the test module only covers config creation. Adding unit tests for get_stream_ws_url (http/https, trailing slash handling) and get_auth_header (expected Basic base64 format) would help prevent regressions without needing network calls.
| auth_header: String, | ||
| #[allow(dead_code)] | ||
| env: HashMap<String, String>, | ||
| command: String, |
There was a problem hiding this comment.
env is currently stored but never read (suppressed via #[allow(dead_code)]). If it’s not needed for streaming, it’s better to remove it from Stream/Stream::new to keep the command focused; otherwise, consider wiring it into the streamed reset/header data similar to record so it serves a purpose.
|
@codex[agent] address pr comments and fix ci |
Addressed the review feedback and fixed the reqwest/json build issue in 30593d2; local cargo test still fails on Linux because windows-only deps require a Windows toolchain. |
|
@copilot ci failing. fix it |
Co-authored-by: ibigbug <543405+ibigbug@users.noreply.github.com>
Fixed in 06a4fbd. Three compilation errors in
|
|
@copilot build failing |
Co-authored-by: ibigbug <543405+ibigbug@users.noreply.github.com>
|
@claude[agent] fix ci and address pr comments |
- Add missing trait imports (base64::Engine, ApiService) in asciinema.rs tests - Update exit message to be accurate (removed Windows-specific Ctrl+Z instruction) - PR review comments already addressed: console_size helper, WS read loop, env field removed Co-authored-by: ibigbug <543405+ibigbug@users.noreply.github.com>
Fixed in 8de0984: CI fixes:
PR review comments:
|
stream subcommand for live terminal session broadcasting|
@copilot update PR title and body |
Updated the PR title to |
stream subcommand for live terminal session broadcasting
Adds a
streamsubcommand to PowerSession-rs that enables live terminal session broadcasting to an asciinema-compatible server over WebSocket.Changes
New
streamsubcommand (src/commands/stream.rs)oevents in real timer(reset) event with the current terminal sizePing/Closeframes and keep the connection alivepowershell.exe(or$SHELLon non-Windows) when no--commandis givenExtended API client (
src/commands/api/asciinema.rs)create_stream(cols, rows)— POST to/api/streamsto create a new live streamget_stream_ws_url(id)— derive the producer WebSocket URL from the server base URLget_auth_header()— return aBasicbase64-encoded Authorization headerget_stream_ws_url(http/https, trailing-slash handling) andget_auth_headerCLI integration (
src/main.rs)streamsubcommand with--command/-c(optional) and--id(optional, reconnects to existing stream)WindowsTerminal::console_size()(lightweight, no PTY allocation) to read terminal dimensionsDependencies
tungstenite0.28 withrustls-tls-webpki-roots— synchronous WebSocket clientreqwestupdated to includejsonfeatureOriginal prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.